/******************************************************************************
 *  _____     _____
 *  \  __\    \  __\
 *   \ \_\\    \ \_\\
 *    \  __\SS  \  __\roject
 *     \_\       \_\
 *
 * Mateusz Maciąg, Rafał Urbański
 * Komputerowe Systemy Sterowania
 *****************************************************************************/

#include "SimulatorSettings.h"
#include "ui_SimulatorSettings.h"

#include "CreateARXDialog.h"
#include "GainControllerDesigner.h"
#include "PIDControllerDesigner.h"
#include "GPCControllerDesigner.h"
#include "RSTControllerDesigner.h"
#include "WrmnkTestControllerDesigner.h"
#include "EmptyControllerDesigner.h"

#include "InputSquareDesigner.h"
#include "InputSineDesigner.h"
#include "InputStepDesigner.h"
#include "InputRandomDesigner.h"
#include "InputSweepDesigner.h"

#include "NonlinDesigners/NLSaturationDesigner.h"

#include "Utils.h"

#include "ARXConfigurator.h"
#include <iostream>
#include <boost/smart_ptr.hpp>

#include <QMessageBox>

#include <utility>

#include <vector>
#include <sstream>

using namespace RafMat::SimInterface;
using namespace RafMat::UI;
using namespace boost;
using namespace std;

SimulatorSettings::SimulatorSettings(MainWindow *parent) :
    QDialog(parent),
    m_mainWindow(parent),
    ui(new Ui::SimulatorSettings)
{
    ui->setupUi(this);

    m_arxConfigurator = new ARXConfigurator(ui->modelContainer);
    ui->modelContainerLayout->addWidget(m_arxConfigurator);

    initializeCtrlTab();
    initializeInputTab();
    initializeOutNLTab();
}

SimulatorSettings::~SimulatorSettings()
{
    delete ui;
}

void SimulatorSettings::initializeCtrlTab()
{
    // Tutaj dajemy możliwość wyboru rodzaju regulatora.

    m_currentCtrlDesigner = 0;
    ui->controllerList->clear();

    ui->controllerList->addItem("Gain regulator (P)",
                                QVariant(static_cast<int>(ISimulator::ControllerTypeGain)));

    ui->controllerList->addItem("PID regulator",
                                QVariant(static_cast<int>(ISimulator::ControllerTypePID)));

    ui->controllerList->addItem("GPC regulator",
                                QVariant(static_cast<int>(ISimulator::ControllerTypeGPC)));

    ui->controllerList->addItem("Dummy WRMNK test regulator",
                                QVariant(static_cast<int>(ISimulator::ControllerTypeWRMNKTest)));

    ui->controllerList->addItem("Adaptive regulator",
                                QVariant(static_cast<int>(ISimulator::ControllerTypeRST)));
}

void SimulatorSettings::initializeInputTab()
{
    // Tutaj dajemy możliwość wyboru konkretnego rodzaju wejścia (uzupełniamy listę).
    // Ważne jest wyzerowanie wskaźnika m_currentInputDesigner, inaczej operator
    // delete się wysypie (w Qt źle się łączy techniki RAII i zręczne wskaźniki z
    // Qtowskimi metodami zarządzania pamięcią, trzeba ręcznie wszystkiego dopilnować).

    m_currentInputDesigner = 0;
    forceControllerViewUpdate = false;
    ui->cbInputTypes->clear();

    ui->cbInputTypes->addItem("Square wave",
                              QVariant(static_cast<int>(ISimulator::InputTypeSquare)));

    ui->cbInputTypes->addItem("Sine wave",
                              QVariant(static_cast<int>(ISimulator::InputTypeSine)));

    ui->cbInputTypes->addItem("Triangle wave",
                              QVariant(static_cast<int>(ISimulator::InputTypeTriangle)));

    ui->cbInputTypes->addItem("Single step",
                              QVariant(static_cast<int>(ISimulator::InputTypeStep)));

    ui->cbInputTypes->addItem("Kronecker pulse",
                              QVariant(static_cast<int>(ISimulator::InputTypeKronecker)));

    ui->cbInputTypes->addItem("Random noise",
                              QVariant(static_cast<int>(ISimulator::InputTypeRandomNoise)));

    ui->cbInputTypes->addItem("Frequency sweep",
                              QVariant(static_cast<int>(ISimulator::InputTypeSweep)));

    ui->cbInputTypes->addItem("Test stairs",
                              QVariant(static_cast<int>(ISimulator::InputTypeKuba)));
}

void SimulatorSettings::initializeOutNLTab()
{
    m_currentNLDesigner = 0;
    forceNLViewUpdate = false;
    ui->cbNonlinType->addItem("Linear output",
                              QVariant(static_cast<int>(ISimulator::NonlinearityNone)));

    ui->cbNonlinType->addItem("Saturation atan(x-off)+a*x",
                              QVariant(static_cast<int>(ISimulator::NonlinearityNLSaturation)));
}

void SimulatorSettings::attachSimulator(ISimulatorSPtr simPtr)
{
    m_simulatorPtr = simPtr;
}

void SimulatorSettings::refreshAll()
{
    ui->modelChain->clear();
    vector<IDiscreteObjectSPtr> objects = m_simulatorPtr->getObjects();
    vector<IDiscreteObjectSPtr>::const_iterator it;

    for(it = objects.begin(); it != objects.end(); ++it)
        insertModelToList(*it);

    vector<IConfigurableSPtr> inputs = m_simulatorPtr->getInputs();
    vector<IConfigurableSPtr>::const_iterator it2;

    ui->inputChain->clear();
    for(it2 = inputs.begin(); it2 != inputs.end(); ++it2)
    {
        QListWidgetItem *item = new QListWidgetItem(ui->inputChain);

        ISimulator::InputMap imap = m_simulatorPtr->getInputMap();

        InputConfPair confPair(*it2, imap[(*it2)->getParameter("type")]);

        item->setData(Qt::UserRole, QVariant::fromValue(confPair));
        item->setText(QString::fromStdString((*it2)->getName()));
    }

    // Odświeżenie widoku regulatora
    updateControllerView();
    updateOutputView();
}

void SimulatorSettings::insertModelToList(IDiscreteObjectSPtr obj)
{
    // Tworzymy nową pozycję w liście modeli i łączymy ją z konfiguracją modelu.
    QListWidgetItem *nItem = new QListWidgetItem(ui->modelChain);
    nItem->setData(Qt::UserRole, QVariant::fromValue(obj));

    string name = ISimulator::iDiscreteObjectToConfigurable(obj)->getName();

    nItem->setText(QString::fromStdString(name));
}

void SimulatorSettings::on_btnInsert_clicked()
{
    if(!m_simulatorPtr.use_count())
    {
        QMessageBox::warning(this, "Internal error.",
                             "Simulator object is not assigned to this window.");
        return;
    }

    int result;

    // Tworzymy tymczasowy obiekt okna dialogowego.
    scoped_ptr<CreateARXDialog> cARX(new CreateARXDialog(this));
    result = cARX->exec();

    if(result) // Wynik kliknięcia w OK lub Anuluj.
    {
        IDiscreteObjectSPtr obj = m_simulatorPtr->createObject(
            cARX->getADegree(),
            cARX->getBDegree(),
            cARX->getCDegree(),
            cARX->getAgDegree(),
            cARX->getD(),
            cARX->getDelay()
        );

        // Tworzymy nową pozycję w liście modeli i łączymy ją z konfiguracją modelu.
//        QListWidgetItem *nItem = new QListWidgetItem(ui->modelChain);
//        nItem->setData(Qt::UserRole, QVariant::fromValue(obj));
//        nItem->setText(cARX->getARXName());
        ISimulator::iDiscreteObjectToConfigurable(obj)->setName(cARX->getARXName().toStdString());
        insertModelToList(obj);

        // Jeśli wybrano preset ustaw jego domyślne ustawienia:
        if(cARX->isPresetSelected())
        {
            try
            {
                IConfigurableSPtr par = obj->getParameters().front();
                ARXPresetSPtr preset = cARX->getPreset();
                par->setName(preset->getObjectName());
                par->setParameter("bpoly", preset->getNumerator());
                par->setParameter("apoly", preset->getDenominator());
                par->setParameter("cpoly", preset->getSDnumerator());
                par->setParameter("delay", preset->getDelay());
                par->setParameter("switch_time", "-1");
                obj->updateParameters();
            }
            catch(std::logic_error &e)
            {
                QMessageBox::warning(this, "Error", e.what());
            }
        }
    }
}

void SimulatorSettings::on_btnDelete_clicked()
{
    if(!m_simulatorPtr.use_count())
    {
        QMessageBox::warning(this, "Internal error.",
                             "Simulator object is not assigned to this window.");
        return;
    }

    QListWidgetItem *nItem = ui->modelChain->currentItem();

    if(nItem == NULL)   // Nie usuwamy niezaznaczonego elementu :)
        return;

    // Wyciągamy dane powiązane z itemem.
    QVariant userData = nItem->data(Qt::UserRole);

    if(userData.isValid())
    {
        IDiscreteObjectSPtr p = userData.value<IDiscreteObjectSPtr>();
        m_simulatorPtr->removeObject(p);
    }

    // Ciekawy i skuteczny sposób na usunięcie elementu z listy:
    delete nItem;
}

void SimulatorSettings::on_modelChain_currentItemChanged(QListWidgetItem *current, QListWidgetItem *)
{
    ui->modelContainer->setEnabled(current != NULL);

    if(current != NULL)
    {
        // Zmiana zaznaczenia w liście modeli
        // Wiąże się z uaktualnieniem okienka konfiguracji modelu.
        QVariant userData = current->data(Qt::UserRole);
        m_arxConfigurator->attachARXObject(userData.value<IDiscreteObjectSPtr>());
    }
}

//
// Sterowanie przebiegiem symulacji
//
void SimulatorSettings::on_btnStart_clicked()
{
    m_mainWindow->startSimTimer();
}

void SimulatorSettings::on_btnStop_clicked()
{
    m_mainWindow->stopSimTimer();
}

void SimulatorSettings::on_btnStep_clicked()
{
    //m_mainWindow->stepSimTimer();
    m_mainWindow->stepSimTimer(ui->edStepSize->value());
}

void SimulatorSettings::on_btnReset_clicked()
{
    m_mainWindow->stopSimTimer();
    m_mainWindow->resetView();
    m_simulatorPtr->reset();

    // External plant - hard reset:
    if(ui->cbEnableExternalPlant->isChecked())
    {
        m_simulatorPtr->enableExternalPlant(false);
        m_simulatorPtr->enableExternalPlant(true);
        on_btnExtApply_clicked();
    }
}

void SimulatorSettings::on_speedSlider_valueChanged(int value)
{
    m_mainWindow->changeSimTiming(1000/value);
    ui->speedLabel->setText(QString("Speed: ") + QString::number(value));
}

// W trakcie modyfikacji - nie zmieniać!
void SimulatorSettings::updateControllerView()
{
    forceControllerViewUpdate = true;

    IConfigurableSPtr ctrlConf = m_simulatorPtr->getController();
    ISimulator::ControllerMap ctrlMap = m_simulatorPtr->getControllerMap();

    std::string ctrlTypeStr = ctrlConf->getParameter("type");
    currentControllerType = ctrlMap[ctrlTypeStr];

    bool checkState = currentControllerType != ISimulator::ControllerTypeOpenLoop;

    if(checkState == ui->cbEnableControl->isChecked())
    {
        // Wymuś zdarzenie toggled, gdyż zmiana niczego nie przyniesie
        on_cbEnableControl_toggled(checkState);
    }
    else
    {
        // W tym przypadku Qt sam wywoła on_cbEnableControl_toggled
        // nie robimy tego drugi raz.
        ui->cbEnableControl->setChecked(checkState);
    }

    // Zdarzenia odświeżenia GUI powinny się od tego momentu same rozpropagować

    forceControllerViewUpdate = false;
}

void SimulatorSettings::updateOutputView()
{
    forceNLViewUpdate = true;

    IConfigurableSPtr nlConf = m_simulatorPtr->getNonlinearityObj();
    ISimulator::NLMap nlMap = m_simulatorPtr->getNonlinearityMap();

    if(nlConf)
    {
        std::string nlTypeStr = nlConf->getParameter("type");
        currentNLType = nlMap[nlTypeStr];
    }
    else
    {
        currentNLType = ISimulator::NonlinearityNone;
    }

    int nlId = ui->cbNonlinType->findData(QVariant(static_cast<int>(currentNLType)));

    if(nlId != ui->cbNonlinType->currentIndex())
    {
        ui->cbNonlinType->setCurrentIndex(nlId);
    }
    else
    {
        on_cbNonlinType_currentIndexChanged(nlId);
    }

    forceNLViewUpdate = false;
}

void SimulatorSettings::on_cbEnableControl_toggled(bool checked)
{
    if(checked)
    {
        int ctrlId;

        // Jeśli jesteśmy w trybie odświeżania widoku, odszukujemy indeks
        // na podstawie istniejącego regulatora
        if(forceControllerViewUpdate)
        {
            ctrlId = ui->controllerList->findData(QVariant(static_cast<int>(currentControllerType)));
        }
        // W przeciwnym wypadku to GUI wymusza tworzenie regulatora wewnątrz symulatora
        else
        {
            ctrlId = ui->controllerList->currentIndex();
        }

        if(ctrlId != ui->controllerList->currentIndex())
        {
            ui->controllerList->setCurrentIndex(ctrlId);
        }
        else
        {
            on_controllerList_currentIndexChanged(ctrlId);
        }
    }
    else
    {
        // Stwórz domyślny regulator blokujący sprzężenie zwrotne.
        // Tzn. OpenLoopController.
        m_simulatorPtr->createController();
    }
}

void SimulatorSettings::on_controllerList_currentIndexChanged(int index)
{
    if(m_simulatorPtr == NULL || index < 0)
        return;

    // Usuń poprzedniego designera.
    delete m_currentCtrlDesigner;
    m_currentCtrlDesigner = 0;

    // Wyciągnij typ regulatora z metadanych.
    int ctrlId = ui->controllerList->itemData(index).toInt();

    // Utwórz designera w zależności od typu wybranego regulatora.
    ISimulator::ControllerType ctype = static_cast<ISimulator::ControllerType>(ctrlId);
    switch(ctype)
    {
    case ISimulator::ControllerTypeGain:
        m_currentCtrlDesigner = new GainControllerDesigner();
        break;
    case ISimulator::ControllerTypePID:
        m_currentCtrlDesigner = new PIDControllerDesigner();
        break;
    case ISimulator::ControllerTypeGPC:
        m_currentCtrlDesigner = new GPCControllerDesigner();
        break;
    case ISimulator::ControllerTypeWRMNKTest:
        m_currentCtrlDesigner = new WrmnkTestControllerDesigner();
        break;
    case ISimulator::ControllerTypeRST:
        m_currentCtrlDesigner = new RSTControllerDesigner();
        break;
    default:
        return;
    }

    // Tworzymy regulator i łączymy go z designerem.
    IConfigurableSPtr iconf;

    if(!forceControllerViewUpdate)
    {
        iconf = m_simulatorPtr->createController(ctype);
    }
    else
    {
        iconf = m_simulatorPtr->getController();
    }

    if(m_currentCtrlDesigner) // przeklęte wskaźniki...
        m_currentCtrlDesigner->attachController(iconf);

    // Dodaj designera do okna konfiguracji.
    ui->controllerSettingsLayout->addWidget(m_currentCtrlDesigner);
}

void SimulatorSettings::on_btnCtrlApplySettings_clicked()
{
    // Zapisz ustawienia regulatora.
    if(m_currentCtrlDesigner != NULL)
    {
        m_currentCtrlDesigner->applySettings();
    }
}

/**
  Reakcja na wciśnięcie Add w zakładce Input.
*/
void SimulatorSettings::on_btnAddInput_clicked()
{
    QVariant data = ui->cbInputTypes->itemData(ui->cbInputTypes->currentIndex());

    // Wyciągnij typ wejścia.
    int itypeId = data.toInt();

    ISimulator::InputType itype = static_cast<ISimulator::InputType>(itypeId);
    IConfigurableSPtr iconf = m_simulatorPtr->addInput(itype);

    // Dodaj wejście do listy i powiąż je z konfiguracją IConfigurable.
    // Do elementu listy doklejamy tak na prawdę parę wartości (std::pair)
    // poniewaz IConfigurable nie przechowuje informacji o typie wejścia (jeszcze).
    QListWidgetItem *item = new QListWidgetItem(ui->inputChain);
    InputConfPair confPair(iconf, itype);

    item->setData(Qt::UserRole, QVariant::fromValue(confPair));
    item->setText(QString::fromStdString(iconf->getName()));
}


void SimulatorSettings::on_inputChain_currentItemChanged(QListWidgetItem *current, QListWidgetItem *)
{
    if(m_simulatorPtr == NULL || current == NULL)
        return;

    delete m_currentInputDesigner;
    m_currentInputDesigner = 0;

    InputConfPair confPair = current->data(Qt::UserRole).value<InputConfPair>();

    // Utworzenie odpowiedniego designera.
    /** \todo Upchać tworzenie designerów w jakiejś fabryce klas. */
    switch(confPair.second)
    {
    case ISimulator::InputTypeSquare:
        m_currentInputDesigner = new InputSquareDesigner();
        break;
    case ISimulator::InputTypeSine:
        m_currentInputDesigner = new InputSineDesigner();
        break;
    case ISimulator::InputTypeTriangle:
        // Używamy tego samego designera co dla sinusa, bo nie wiele się różni parametrami.
        m_currentInputDesigner = new InputSineDesigner();
        break;
    case ISimulator::InputTypeStep:
        m_currentInputDesigner = new InputStepDesigner();
        break;
    case ISimulator::InputTypeKronecker:
        // Używamy tego samego designera co dla step, bo nie wiele się różni parametrami.
        m_currentInputDesigner = new InputStepDesigner();
        break;
    case ISimulator::InputTypeRandomNoise:
        // Używamy tego samego designera co dla step, bo nie wiele się różni parametrami.
        m_currentInputDesigner = new InputRandomDesigner();
        break;
    case ISimulator::InputTypeSweep:
        m_currentInputDesigner = new InputSweepDesigner();
        break;
    case ISimulator::InputTypeKuba:
        m_currentInputDesigner = NULL;
        break;
    default:
        return;
    }

    if(m_currentInputDesigner)
    {
        // Łączenie designera z konfiguracją wejścia.
        m_currentInputDesigner->attachInput(confPair.first);

        // Dodanie designera do GUI.
        ui->gbInputSettingsLayout->addWidget(m_currentInputDesigner);
    }
    ui->inputName->setText(QString::fromStdString(confPair.first->getName()));
}

void SimulatorSettings::on_btnApplyInput_clicked()
{
    if(m_currentInputDesigner != 0)
    {
        m_currentInputDesigner->applySettings();

        // Modyfikacja wymaga odświeżenia pozycji w liście (nazwy)
        QListWidgetItem *currentItem = ui->inputChain->currentItem();
        IConfigurableSPtr currentConf = currentItem->data(Qt::UserRole).value<InputConfPair>().first;
        QString txt = ui->inputName->text();
        currentConf->setName(txt.toStdString());
        currentItem->setText(txt);
    }
}

void SimulatorSettings::on_btnRemoveInput_clicked()
{
    QListWidgetItem *currentItem = ui->inputChain->currentItem();

    if(currentItem == NULL)
        return;

    // Usuń wejście wraz z jego designerem.
    IConfigurableSPtr currentConf = currentItem->data(Qt::UserRole).value<InputConfPair>().first;
    m_simulatorPtr->removeInput(currentConf);

    delete m_currentInputDesigner;
    m_currentInputDesigner = 0;

    delete currentItem;
}

void SimulatorSettings::on_cbNonlinType_currentIndexChanged(int index)
{
    if(m_simulatorPtr == NULL || index < 0)
        return;

    // Usuń poprzedniego designera.
    delete m_currentNLDesigner;
    m_currentNLDesigner = 0;

    // Wyciągnij typ regulatora z metadanych.
    int ctrlId = ui->cbNonlinType->itemData(index).toInt();

    // Utwórz designera w zależności od typu wybranego regulatora.
    ISimulator::NonlinearityType ctype = static_cast<ISimulator::NonlinearityType>(ctrlId);
    switch(ctype)
    {
    case ISimulator::NonlinearityNone:
        break;

    case ISimulator::NonlinearityNLSaturation:
        m_currentNLDesigner = new NLSaturationDesigner();
        break;

    default:
        return;
    }

    // Tworzymy regulator i łączymy go z designerem.
    IConfigurableSPtr iconf;

    if(forceNLViewUpdate)
        iconf = m_simulatorPtr->getNonlinearityObj();
    else
        iconf = m_simulatorPtr->setNonlinearity(ctype);

    if(m_currentNLDesigner && iconf) // przeklęte wskaźniki...
        m_currentNLDesigner->attachModel(iconf);

    // Dodaj designera do okna konfiguracji.
    ui->nonlinearitySettingsLayout->addWidget(m_currentNLDesigner);
}

void SimulatorSettings::on_btnApplyNLSettings_clicked()
{
    if(m_currentNLDesigner)
    {
        m_currentNLDesigner->applySettings();
    }
}

void SimulatorSettings::on_edHistoryDepth_valueChanged(int arg1)
{
    if(m_mainWindow == NULL)
        return;

    m_mainWindow->changePlotBufferSize(arg1);
    m_mainWindow->replot();
}

void SimulatorSettings::on_cbPlotStyle_currentIndexChanged(const QString &arg1)
{
    if(m_mainWindow == NULL)
        return;

    if(arg1 == "Lines")
        m_mainWindow->changePlotStyle(QwtPlotCurve::Lines);
    else if(arg1 == "Steps")
        m_mainWindow->changePlotStyle(QwtPlotCurve::Steps);
    else if(arg1 == "Dots")
        m_mainWindow->changePlotStyle(QwtPlotCurve::Dots);
    else if(arg1 == "Sticks")
        m_mainWindow->changePlotStyle(QwtPlotCurve::Sticks);

    m_mainWindow->replot();
}


void SimulatorSettings::on_cbAntiAliasing_clicked(bool checked)
{
    if(m_mainWindow == NULL)
        return;

    m_mainWindow->setAntiAliasing(checked);
    m_mainWindow->replot();
}

void SimulatorSettings::on_doubleSpinBox_valueChanged(double arg1)
{
    if(m_mainWindow == NULL)
        return;

    m_mainWindow->setPlotWidth(arg1);
}

void SimulatorSettings::on_cbEnableExternalPlant_toggled(bool checked)
{
    m_simulatorPtr->enableExternalPlant(checked);

    // Force parameter update
    on_btnExtApply_clicked();
}

using namespace std;

SimulatorSettings::StringPairVector SimulatorSettings::parseExtParams(string text)
{
    istringstream istr(text);
    StringPairVector outVect;
    string arg, val;

    while(true)
    {
        istr >> arg >> val;

        if(istr.bad())
            break;

        if(!arg.empty() && !val.empty())
            outVect.push_back(StringPair(arg, val));

        if(istr.eof())
            break;
    }

    return outVect;
}

void SimulatorSettings::on_btnExtApply_clicked()
{
    bool checked = ui->cbEnableExternalPlant->isChecked();
    IConfigurableSPtr plantConf = m_simulatorPtr->getExternalPlant();

    if(checked && plantConf)
    {
        try
        {
            plantConf->setParameter("samplePeriod", ui->edExtSamplePeriod->text().toStdString());

            StringPairVector userParams = parseExtParams(ui->edExtUserParams->toPlainText().toStdString());

            for(StringPairVector::iterator it = userParams.begin(); it != userParams.end(); ++it)
            {
                plantConf->setParameter(it->first, it->second);
            }
        }
        catch(std::exception &exc)
        {
            QMessageBox::warning(this, "External model error.",
                                 exc.what());
            return;
        }

    }
}

void SimulatorSettings::on_cbEnableCapture_toggled(bool checked)
{
    if(!checked)
        on_btnStopCapture_clicked();
}

void SimulatorSettings::on_btnStartCapture_clicked()
{
    m_simulatorPtr->enableDataCapture(ui->cbEnableCapture->isChecked());
}

void SimulatorSettings::on_btnStopCapture_clicked()
{
    m_simulatorPtr->enableDataCapture(false);
}

void SimulatorSettings::on_fileCapturePath_editingFinished()
{
    m_simulatorPtr->setCaptureFilePath(ui->fileCapturePath->text().toStdString());
}
